/*
 * Copyright 2011 myTDev.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mytdev.application;

import com.mytdev.application.service.ServiceDescriptor;
import com.mytdev.application.service.ServiceProvider;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JFrame;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public final class XMLApplicationEngineBuilder implements ApplicationEngineBuilder {

    private static final String APPLICATION_DESCRIPTOR_PATH = "/META-INF/application/application.xml";
    private static final String APPLICATION_DESCRIPTOR_SCHEMA_PATH = "/com/mytdev/application/resources/application.xsd";
    private static final XMLApplicationEngineBuilder INSTANCE = new XMLApplicationEngineBuilder();
    private static final Logger LOG = LoggerFactory.getLogger(XMLApplicationEngineBuilder.class);

    public XMLApplicationEngineBuilder() {
    }

    public static XMLApplicationEngineBuilder getInstance() {
        return INSTANCE;
    }

    @Override
    public ApplicationEngine build() throws ApplicationException {
        URL documentURL = XMLApplicationEngineBuilder.class.getResource(APPLICATION_DESCRIPTOR_PATH);
        if (documentURL == null) {
            throw new ApplicationException("application descriptor file not found: " + APPLICATION_DESCRIPTOR_PATH);
        }
        Document document = getDocument(documentURL);
        ApplicationMetaData appMetaData = parseApplicationMetaData(document);
        Map<String, ServiceDescriptor> services = parseServices(document);
        Map<String, String> properties = parseProperties(document);
        Class<? extends JFrame> mainView = parseMainView(document);
        return new ApplicationEngine(appMetaData, services, mainView, properties);
    }

    private Document getDocument(URL url) throws ApplicationException {
        Document document = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setValidating(false);
            factory.setNamespaceAware(true);
            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            Schema schema = schemaFactory.newSchema(XMLApplicationEngineBuilder.class.getResource(APPLICATION_DESCRIPTOR_SCHEMA_PATH));
            factory.setSchema(schema);
            DocumentBuilder builder = factory.newDocumentBuilder();
            document = builder.parse(url.toString());
        } catch (Exception ex) {
            throw new ApplicationException("error loading descriptor: " + ex.getMessage(), ex);
        }
        return document;
    }

    private ApplicationMetaData parseApplicationMetaData(Document document) throws ApplicationException {
        Element root = document.getDocumentElement();
        String name = root.getElementsByTagName(elements.name).item(0).getTextContent();
        String version = root.getElementsByTagName(elements.version).item(0).getTextContent();
        String description = null;
        NodeList nodeList = root.getElementsByTagName(elements.description);
        if (nodeList.getLength() > 0) {
            description = nodeList.item(0).getTextContent();
        }
        String vendor = null;
        nodeList = root.getElementsByTagName(elements.vendor);
        if (nodeList.getLength() > 0) {
            vendor = nodeList.item(0).getTextContent();
        }
        String website = null;
        nodeList = root.getElementsByTagName(elements.website);
        if (nodeList.getLength() > 0) {
            website = nodeList.item(0).getTextContent();
        }
        return new ApplicationMetaData(name, version, description, vendor, website);
    }

    private Map<String, ServiceDescriptor> parseServices(Document document) throws ApplicationException {
        Map<String, ServiceDescriptor> services = new HashMap<String, ServiceDescriptor>();
        NodeList nodeList = document.getDocumentElement().getElementsByTagName(elements.service);
        for (int index = 0; index < nodeList.getLength(); index++) {
            ServiceDescriptor service = parseService((Element) nodeList.item(index));
            services.put(service.getName(), service);
        }
        nodeList = document.getElementsByTagName(elements.services_file);
        for (int index = 0; index < nodeList.getLength(); index++) {
            Element element = (Element) nodeList.item(index);
            String file = element.getAttribute(attributes.file);
            URL url = XMLApplicationEngineBuilder.class.getResource(file);
            if (url == null) {
                throw new ApplicationException("services descriptor file not found: " + file);
            }
            services.putAll(parseServices(getDocument(url)));
        }
        return services;
    }

    private ServiceDescriptor parseService(Element serviceElement) throws ApplicationException {
        String name = serviceElement.getAttribute(attributes.name);
        Class<? extends ServiceProvider<?>> provider = null;
        String providerName = serviceElement.getAttribute(attributes.provider);
        try {
            provider = (Class<? extends ServiceProvider<?>>) Class.forName(providerName);
        } catch (ClassNotFoundException ex) {
            throw new ApplicationException("unknown service provider " + providerName, ex);
        }
        Map<String, Object> properties = new HashMap<String, Object>();
        NodeList nodeList = serviceElement.getElementsByTagName(elements.property);
        for (int index = 0; index < nodeList.getLength(); index++) {
            Element propertyElement = (Element) nodeList.item(index);
            String propertyName = propertyElement.getAttribute(attributes.name);
            String propertyValue = propertyElement.getAttribute(attributes.value);
            properties.put(propertyName, propertyValue);
        }
        List<String> dependencies = new ArrayList<String>();
        nodeList = serviceElement.getElementsByTagName(elements.dependency);
        for (int index = 0; index < nodeList.getLength(); index++) {
            dependencies.add(nodeList.item(index).getTextContent());
        }
        boolean cacheable = Boolean.parseBoolean(
                serviceElement.getAttribute(attributes.cacheable));
        boolean loadOnStartUp = Boolean.parseBoolean(
                serviceElement.getAttribute(attributes.loadOnStartUp));
        Class<?> serviceType = null;
        try {
            serviceType = provider.getMethod("getService").getReturnType();
        } catch (Exception ex) {
            throw new ApplicationException(ex.getLocalizedMessage(), ex);
        }
        return new ServiceDescriptor(name, provider, serviceType, cacheable, loadOnStartUp, properties, dependencies);
    }

    private Map<String, String> parseProperties(Document document) {
        Map<String, String> properties = new HashMap<String, String>();
        NodeList nodeList = document.getDocumentElement().getElementsByTagName(elements.properties);
        if (nodeList.getLength() > 0) {
            Element propertiesElement = (Element) nodeList.item(0);
            nodeList = propertiesElement.getElementsByTagName(elements.property);
            for (int index = 0; index < nodeList.getLength(); index++) {
                Element element = (Element) nodeList.item(index);
                String name = element.getAttribute(attributes.name);
                String value = element.getAttribute(attributes.value);
                properties.put(name, value);
            }
        }
        return properties;
    }

    private Class<? extends JFrame> parseMainView(Document document) throws ApplicationException {
        LOG.debug("parsing main view");
        Class<? extends JFrame> mainView = null;
        NodeList nodeList = document.getDocumentElement().getElementsByTagName(elements.view);
        if (nodeList.getLength() > 0) {
            Element mainViewElement = (Element) nodeList.item(0);
            try {
                mainView = (Class<? extends JFrame>) Class.forName(mainViewElement.getTextContent());
            } catch (ClassNotFoundException ex) {
                throw new ApplicationException("unknown view", ex);
            }
        }
        LOG.debug("main view is " + mainView);
        return mainView;
    }

    private interface elements {

        String name = "name";
        String version = "version";
        String description = "description";
        String vendor = "vendor";
        String website = "website";
        String services = "services";
        String service = "service";
        String services_file = "services-file";
        String dependency = "dependency";
        String properties = "properties";
        String property = "property";
        String view = "view";
        String splash = "splash";
    }

    private interface attributes {

        String cacheable = "cacheable";
        String file = "file";
        String loadOnStartUp = "loadOnStartUp";
        String name = "name";
        String provider = "provider";
        String value = "value";
    }
}
